home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-9.10-netbook-remix-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / aptdaemon / progress.py < prev    next >
Text File  |  2009-10-14  |  18KB  |  481 lines

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """Progress handlers for APT operations"""
  4. # Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
  5. #
  6. # Licensed under the GNU General Public License Version 2
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12.  
  13. __author__  = "Sebastian Heinlein <devel@glatzor.de>"
  14.  
  15. from gettext import gettext as _
  16. import locale
  17. import logging
  18. import os
  19. import re
  20. import subprocess
  21. import sys
  22. import termios
  23. import time
  24. import threading
  25. import tty
  26. import warnings
  27.  
  28. import apt_pkg
  29. import apt.progress
  30. import apt.debfile
  31. import gobject
  32.  
  33. import enums
  34. from loop import mainloop
  35.  
  36. log = logging.getLogger("AptDaemon.Worker")
  37. log_terminal = logging.getLogger("AptDaemon.Worker.Terminal")
  38.  
  39. INSTALL_TIMEOUT = 10 * 60
  40.  
  41. MAP_STAGE = {"install":_("Installing %s"),
  42.              "configure":_("Configuring %s"),
  43.              "remove":_("Removing %s"),
  44.              "trigproc":_("Running post-installation trigger %s"),
  45.              "purge":_("Purging %s"),
  46.              "upgrade":_("Upgrading %s")}
  47.  
  48. REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]"
  49.  
  50. class DaemonOpenProgress(apt.progress.OpProgress):
  51.  
  52.     """Handles the progress of the cache opening."""
  53.  
  54.     def __init__(self, transaction, begin=0, end=100, quiet=False):
  55.         """Initialize a new DaemonOpenProgress instance.
  56.  
  57.         Keyword arguments:
  58.         transaction -- corresponding transaction D-Bus object
  59.         begin -- begin of the progress range (defaults to 0)
  60.         end -- end of the progress range (defaults to 100)
  61.         quiet -- do not emit any progress information for the transaction
  62.         """
  63.         apt.progress.OpProgress.__init__(self)
  64.         self._transaction = transaction
  65.         self.steps = [begin + (end - begin) * modifier
  66.                       for modifier in [0.12, 0.25, 0.50, 0.75, 1.00]]
  67.         self.progress_begin = float(begin)
  68.         self.progress_end = self.steps.pop(0)
  69.         self.progress = 0
  70.         self.quiet = quiet
  71.  
  72.     def update(self, percent):
  73.         """Callback for progress updates.
  74.  
  75.         Keyword argument:
  76.         percent - current progress in percent
  77.         """
  78.         progress = int(self.progress_begin + percent / 100 * \
  79.                        (self.progress_end - self.progress_begin))
  80.         if self.progress < progress:
  81.             if not self.quiet:
  82.                 self._transaction.progress = progress
  83.             self.progress = progress
  84.         while gobject.main_context_default().pending():
  85.             gobject.main_context_default().iteration()
  86.  
  87.     def done(self):
  88.         """Callback after completing a step.
  89.  
  90.         Sets the progress range to the next interval."""
  91.         self.progress_begin = self.progress_end
  92.         try:
  93.             self.progress_end = self.steps.pop(0)
  94.         except:
  95.             log.warning("An additional step to open the cache is required")
  96.  
  97.  
  98. class DaemonFetchProgress(apt.progress.FetchProgress):
  99.     '''
  100.     Handle the package download process
  101.     '''
  102.     def __init__(self, transaction, begin=0, end=100):
  103.         apt.progress.FetchProgress.__init__(self)
  104.         self.transaction = transaction
  105.         self.progress_end = end
  106.         self.progress_begin = begin
  107.         self.progress = 0
  108.         self.items = {}
  109.         #FIXME: This should already be part of python-apt
  110.         self.currentItems = 0
  111.         self.totalItems = 0
  112.         self.currentBytes = 0
  113.         self.totalBytes = 0
  114.         self.currentCPS = 0
  115.  
  116.     def pulse(self):
  117.         """Callback to update progress information"""
  118.         apt.progress.FetchProgress.pulse(self)
  119.         if self.transaction.cancelled:
  120.             return False
  121.         self.transaction.progress_details = (self.currentItems, self.totalItems,
  122.                                              self.currentBytes, self.totalBytes,
  123.                                              self.currentCPS, self.eta)
  124.         progress = int(self.progress_begin + self.percent/100 * \
  125.                        (self.progress_end - self.progress_begin))
  126.         # If the progress runs backwards emit an illegal progress value
  127.         # e.g. during cache updates.
  128.         if self.progress > progress:
  129.             self.transaction.progress = 101
  130.         else:
  131.             self.transaction.progress = progress
  132.             self.progress = progress
  133.         while gobject.main_context_default().pending():
  134.             gobject.main_context_default().iteration()
  135.         return True
  136.  
  137.     def updateStatus(self, uri, descr, shortDescr, status):
  138.         """Callback to update the status information"""
  139.         if status != self.dlQueued:
  140.             log.debug("%s %s" % (self.dlStatusStr[status], uri))
  141.         if status == self.dlQueued:
  142.             self.transaction.status_details = _("Downloading %s") % shortDescr
  143.         self.items[uri] = status
  144.  
  145.     def start(self):
  146.         """Callback at the beginning of the operation"""
  147.         self.transaction.status = enums.STATUS_DOWNLOADING
  148.         self.transaction.allow_cancel = True
  149.  
  150.     def stop(self):
  151.         """Callback at the end of the operation"""
  152.         self.transaction.progress_details = (0, 0, 0, 0, 0, 0)
  153.         self.transaction.progress = self.progress_end
  154.         self.transaction.allow_cancel = False
  155.  
  156.     def mediaChange(self, medium, drive):
  157.         """Callback for media changes"""
  158.         #FIXME: make use of DeviceKit/hal
  159.         self.transaction.required_medium = medium, drive
  160.         self.transaction.paused = True
  161.         self.transaction.status = enums.STATUS_WAITING_MEDIUM
  162.         while self.transaction.paused:
  163.             gobject.main_context_default().iteration()
  164.         self.transaction.status = enums.STATUS_DOWNLOADING
  165.         if self.transaction.cancelled:
  166.             return False
  167.         return True
  168.  
  169.  
  170. class DaemonInstallProgress(object):
  171.  
  172.     def __init__(self, transaction, begin=50, end=100):
  173.         self.transaction = transaction
  174.         self.status = ""
  175.         self.progress = 0
  176.         self.progress_begin = begin
  177.         self.progress_end = end
  178.         self._child_exit = -1
  179.         self.last_activity = 0
  180.         self.child_pid = 0
  181.         self.status_parent_fd, self.status_child_fd = os.pipe()
  182.         self.output = ""
  183.         self._line_buffer = ""
  184.  
  185.     def startUpdate(self):
  186.         log.debug("Start update")
  187.         try:
  188.             apt_pkg.PkgSystemUnLock()
  189.         except SystemError:
  190.             pass
  191.         self.transaction.status = enums.STATUS_COMMITTING
  192.         self.transaction.allow_terminal = True
  193.         self.last_activity = time.time()
  194.         self.start_time = time.time()
  195.  
  196.     def finishUpdate(self):
  197.         """Callback at the end of the operation"""
  198.         #if self.conffile_prompts:
  199.         #    self._transaction.Message(MESSAGE_CONFIG_FILES_CHANGED, 
  200.         #                          "The following conffile prompts were found "
  201.         #                          "and need investiagtion: %s" % \
  202.         #                          "\n".join(self.conffile_prompts))
  203.         # Check for required restarts
  204.         #if os.path.exists("/var/run/reboot-required") and \
  205.         #   os.path.getmtime("/var/run/reboot-required") > self.start_time:
  206.         #self._transaction.RequireRestart(RESTART_SYSTEM, "")
  207.         self.transaction.allow_terminal = False
  208.  
  209.     def _child(self, pm):
  210.         try:
  211.             res = pm.DoInstall(self.status_child_fd)
  212.         except:
  213.             os._exit(pm.ResultFailed)
  214.         else:
  215.             os._exit(res)
  216.  
  217.     def run(self, *args, **kwargs):
  218.         log.debug("Run")
  219.         pid = self._fork()
  220.         if pid == 0:
  221.             os.close(self.status_parent_fd)
  222.             self._child(*args, **kwargs)
  223.         else:
  224.             self.child_pid = pid
  225.             os.close(self.status_child_fd)
  226.         log.debug("Child pid: %s", pid)
  227.         watchers = []
  228.         flags = gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP
  229.         if self.transaction.terminal:
  230.             # Save the settings of the transaction terminal and set to raw mode
  231.             terminal_fd = os.open(self.transaction.terminal,
  232.                                   os.O_RDWR|os.O_NOCTTY|os.O_NONBLOCK)
  233.             terminal_attr = termios.tcgetattr(terminal_fd)
  234.             tty.setraw(terminal_fd, termios.TCSANOW)
  235.             # Setup copying of i/o between the controlling terminals
  236.             watchers.append(gobject.io_add_watch(terminal_fd, flags,
  237.                                                  self._copy_io))
  238.         else:
  239.             terminal_fd = None
  240.         watchers.append(gobject.io_add_watch(self.master_fd, flags,
  241.                                              self._copy_io_master, terminal_fd))
  242.         # Monitor the child process
  243.         watchers.append(gobject.child_watch_add(pid, self._on_child_exit))
  244.         # Watch for status updates
  245.         watchers.append(gobject.io_add_watch(self.status_parent_fd,
  246.                                              gobject.IO_IN,
  247.                                              self._on_status_update))
  248.         while self._child_exit == -1:
  249.             gobject.main_context_default().iteration()
  250.         for id in watchers:
  251.             gobject.source_remove(id)
  252.         # Restore the settings of the transaction terminal
  253.         try:
  254.             termios.tcsettattr(terminal_fd, termios.TCSADRAIN, terminal_attr)
  255.         except:
  256.             pass
  257.         # Make sure all file descriptors are closed
  258.         for fd in [self.master_fd, self.status_parent_fd, terminal_fd]:
  259.             try:
  260.                 os.close(fd)
  261.             except:
  262.                 pass
  263.         return os.WEXITSTATUS(self._child_exit)
  264.  
  265.     def _on_child_exit(self, pid, condition):
  266.         log.debug("Child exited: %s", condition)
  267.         self._child_exit = condition
  268.         return False
  269.  
  270.     def _on_status_update(self, source, condition):
  271.         log.debug("UpdateInterface")
  272.         status_msg = ""
  273.         try:
  274.             while not status_msg.endswith("\n"):
  275.                 self.last_activity = time.time()
  276.                 status_msg += os.read(source, 1)
  277.         except:
  278.             return False
  279.         try:
  280.             (status, pkg, percent, message_raw) = status_msg.split(":", 3)
  281.         except ValueError:
  282.             # silently ignore lines that can't be parsed
  283.             return True
  284.         message = message_raw.strip()
  285.         #print "percent: %s %s" % (pkg, float(percent)/100.0)
  286.         if status == "pmerror":
  287.             self._error(pkg, message)
  288.         elif status == "pmconffile":
  289.             # we get a string like this:
  290.             # 'current-conffile' 'new-conffile' useredited distedited
  291.             match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", message_raw)
  292.             if match:
  293.                 new, old = match.group(1), match.group(2)
  294.                 self._conffile(new, old)
  295.         elif status == "pmstatus":
  296.             self._status_changed(pkg, float(percent), message)
  297.         # catch a time out by sending crtl+c
  298.         if self.last_activity + INSTALL_TIMEOUT < time.time() and \
  299.            self.child_pid:
  300.             log.critical("Killing child since timeout of %s s", INSTALL_TIMEOUT)
  301.             os.kill(self.child_pid, 15)
  302.         return True
  303.  
  304.     def _fork(self):
  305.         """Fork and create a master/slave pty pair by which the forked process
  306.         can be controlled.
  307.         """
  308.         # process all pending events in the main loop, since we will quit
  309.         # the loop in the child process
  310.         context = gobject.main_context_default()
  311.         while context.pending():
  312.             context.iteration()
  313.         pid, self.master_fd = os.forkpty()
  314.         if pid == 0:
  315.             mainloop.quit()
  316.             # Switch to the language of the user
  317.             if self.transaction.locale:
  318.                 os.putenv("LANG", self.transaction.locale)
  319.             # Either connect to the controllong terminal or switch to
  320.             # non-interactive mode
  321.             if not self.transaction.terminal:
  322.                 # FIXME: we should check for "mail" or "gnome" here
  323.                 #        and not unset in this case
  324.                 os.putenv("APT_LISTCHANGES_FRONTEND", "none")
  325.             else:
  326.                 #FIXME: Should this be a setting?
  327.                 os.putenv("TERM", "linux")
  328.             # Run debconf through a proxy if available
  329.             if self.transaction.debconf:
  330.                 os.putenv("DEBCONF_PIPE", self.transaction.debconf)
  331.                 os.putenv("DEBIAN_FRONTEND", "passthrough")
  332.                 if log.level == logging.DEBUG:
  333.                     os.putenv("DEBCONF_DEBUG",".")
  334.             elif not self.transaction.terminal:
  335.                 os.putenv("DEBIAN_FRONTEND", "noninteractive")
  336.         return pid
  337.  
  338.     def _copy_io_master(self, source, condition, target):
  339.         if condition == gobject.IO_IN:
  340.             self.last_activity = time.time()
  341.             char = os.read(source, 1)
  342.             # Write all the output from dpkg to a log
  343.             if char == "\n":
  344.                 # Skip ANSI characters from the console output
  345.                 line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer)
  346.                 if line:
  347.                     log_terminal.debug(line)
  348.                     self.output += line + "\n"
  349.                 self._line_buffer = ""
  350.             else:
  351.                 self._line_buffer += char
  352.             if target:
  353.                 try:
  354.                     os.write(target, char)
  355.                 except:
  356.                     pass
  357.             return True
  358.         os.close(source)
  359.         return False
  360.  
  361.     def _copy_io(self, source, condition):
  362.         if condition == gobject.IO_IN:
  363.             char = os.read(source, 1)
  364.             # Detect config file prompt answers on the console
  365.             # FIXME: Perhaps should only set the
  366.             # self.transaction.config_file_prompt_answer and not write
  367.             if self.transaction.paused and \
  368.                self.transaction.config_file_prompt:
  369.                 self.transaction.config_file_prompt_answer = None
  370.                 self.transaction.paused = False
  371.             try:
  372.                 os.write(self.master_fd, char)
  373.             except:
  374.                 pass
  375.             else:
  376.                 return True
  377.         os.close(source)
  378.         return False
  379.  
  380.     def _status_changed(self, pkg, percent, status):
  381.         """Callback to update status information"""
  382.         log.debug("APT status: %s" % status)
  383.         progress = self.progress_begin + percent / 100 * \
  384.                    (self.progress_end - self.progress_begin)
  385.         if self.progress < progress:
  386.             self.transaction.progress = int(progress)
  387.             self.progress = progress
  388.         self.transaction.status_details = status
  389.  
  390.     def _conffile(self, current, new):
  391.         """Callback for a config file conflict"""
  392.         log.warning("Config file prompt: '%s' (%s)" % (current, new))
  393.         self.transaction.config_file_prompt = (current, new)
  394.         self.transaction.paused = True
  395.         self.transaction.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT
  396.         while self.transaction.paused:
  397.             gobject.main_context_default().iteration()
  398.         log.info("Sending config file answer: %s",
  399.                  self.transaction.config_file_prompt_answer)
  400.         if self.transaction.config_file_prompt_answer == "replace":
  401.             os.write(self.master_fd, "y\n")
  402.         elif self.transaction.config_file_prompt_answer == "keep":
  403.             os.write(self.master_fd, "n\n")
  404.         self.transaction.config_file_prompt_answer = None
  405.         self.transaction.config_file_prompt = None
  406.         self.transaction.status = enums.STATUS_COMMITTING
  407.         return True
  408.  
  409.     def _error(self, pkg, msg):
  410.         """Callback for an error"""
  411.         log.critical("%s: %s" % (pkg, msg))
  412.  
  413.  
  414. class DaemonDpkgInstallProgress(DaemonInstallProgress):
  415.  
  416.     """Progress handler for a local Debian package installation."""
  417.  
  418.     def _child(self, debfile):
  419.          args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd)]
  420.          if not self.transaction.terminal:
  421.              args.extend(["--force-confdef", "--force-confold"])
  422.          args.extend(["-i", debfile])
  423.          os.execlp("/usr/bin/dpkg", *args)
  424.          # We should never go here
  425.          os._exit()
  426.  
  427.     def _on_status_update(self, source, condition):
  428.         log.debug("UpdateInterface")
  429.         status_raw = ""
  430.         try:
  431.             while not status_raw.endswith("\n"):
  432.                 status_raw += os.read(source, 1)
  433.         except:
  434.             return False
  435.         try:
  436.             status = [s.strip() for s in status_raw.split(":", 3)]
  437.         except ValueError:
  438.             # silently ignore lines that can't be parsed
  439.             return True
  440.         # Parse the status message. It can be of the following types:
  441.         #  - "status: PACKAGE: STATUS"
  442.         #  - "status: PACKAGE: error: MESSAGE"
  443.         #  - "status: FILE: conffile: 'OLD' 'NEW' useredited distedited"
  444.         #  - "processing: STAGE: PACKAGE" with STAGE is one of upgrade,
  445.         #    install, configure, trigproc, remove, purge
  446.         if status[0] == "status":
  447.             if status[2] == "error":
  448.                 self._error(status[1], status[3])
  449.             elif status[2] == "conffile":
  450.                 match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status[3])
  451.                 if match:
  452.                     new, old = match.group(1), match.group(2)
  453.                     self._conffile(new, old)
  454.             elif status == "status":
  455.  
  456.                 self._status_changed(pkg=status[1], percent=0,
  457.                                      status=status[2])
  458.         elif status[0] == "processing":
  459.             try:
  460.                 msg = MAP_STAGE[status[1]] % status[2]
  461.             except ValueError, IndexError:
  462.                 msg = status[1]
  463.             self._status_changed(pkg=status[2], percent=0, status=msg)
  464.  
  465.  
  466. class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress):
  467.  
  468.     """Progress handler for dpkg --confiure -a call."""
  469.  
  470.     def _child(self):
  471.         args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd),
  472.                 "--configure", "-a"]
  473.         if not self.transaction.terminal:
  474.             args.extend(["--force-confdef", "--force-confold"])
  475.         os.execlp("/usr/bin/dpkg", *args)
  476.         # We should never go here
  477.         os._exit()
  478.  
  479.  
  480. # vim:ts=4:sw=4:et
  481.